Objevte TypeScript branded types, techniku pro nominální typování ve strukturálním systému. Zvyšte typovou bezpečnost a srozumitelnost kódu.
TypeScript Branded Types: Nominální typování ve strukturálním systému
Strukturální typový systém TypeScriptu nabízí flexibilitu, ale někdy může vést k neočekávanému chování. Branded types poskytují způsob, jak vynutit nominální typování, čímž zvyšují typovou bezpečnost a srozumitelnost kódu. Tento článek podrobně prozkoumává branded types a poskytuje praktické příklady a osvědčené postupy pro jejich implementaci.
Porozumění strukturálnímu a nominálnímu typování
Než se ponoříme do branded types, ujasněme si rozdíl mezi strukturálním a nominálním typováním.
Strukturální typování (Duck Typing)
Ve strukturálním typovém systému jsou dva typy považovány za kompatibilní, pokud mají stejnou strukturu (tj. stejné vlastnosti se stejnými typy). TypeScript používá strukturální typování. Zvažte tento příklad:
interface Point {
x: number;
y: number;
}
interface Vector {
x: number;
y: number;
}
const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // V TypeScriptu platné
console.log(vector.x); // Výstup: 10
I když jsou Point
a Vector
deklarovány jako odlišné typy, TypeScript umožňuje přiřadit objekt typu Point
proměnné typu Vector
, protože sdílejí stejnou strukturu. To může být pohodlné, ale může to také vést k chybám, pokud potřebujete rozlišovat mezi logicky odlišnými typy, které mají náhodou stejný tvar. Například souřadnice zeměpisné šířky/délky, které by se mohly náhodně shodovat se souřadnicemi pixelů na obrazovce.
Nominální typování
V nominálním typovém systému jsou typy považovány za kompatibilní pouze tehdy, mají-li stejné jméno. I když mají dva typy stejnou strukturu, jsou považovány za odlišné, pokud mají různá jména. Jazyky jako Java a C# používají nominální typování.
Potřeba Branded Types
Strukturální typování TypeScriptu může být problematické, když potřebujete zajistit, aby hodnota patřila ke konkrétnímu typu bez ohledu na její strukturu. Zvažte například reprezentaci měn. Můžete mít různé typy pro USD a EUR, ale oba by mohly být reprezentovány jako čísla. Bez mechanismu pro jejich rozlišení byste mohli omylem provádět operace se špatnou měnou.
Branded types tento problém řeší tím, že vám umožňují vytvářet odlišné typy, které jsou strukturálně podobné, ale typovým systémem jsou považovány za různé. To zvyšuje typovou bezpečnost a předchází chybám, které by jinak mohly proklouznout.
Implementace Branded Types v TypeScriptu
Branded types jsou implementovány pomocí průnikových typů (intersection types) a jedinečného symbolu nebo řetězcového literálu. Myšlenka spočívá v přidání "značky" (brand) k typu, která jej odliší od ostatních typů se stejnou strukturou.
Použití symbolů (doporučeno)
Použití symbolů pro branding je obecně preferováno, protože symboly jsou zaručeně jedinečné.
const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };
const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Celkem USD:", totalUSD);
// Odkomentování následujícího řádku způsobí typovou chybu
// const invalidOperation = addUSD(usd1, eur1);
V tomto příkladu jsou USD
a EUR
branded types založené na typu number
. unique symbol
zajišťuje, že tyto typy jsou odlišné. Funkce createUSD
a createEUR
se používají k vytváření hodnot těchto typů a funkce addUSD
přijímá pouze hodnoty typu USD
. Pokus o přičtení hodnoty EUR
k hodnotě USD
povede k typové chybě.
Použití řetězcových literálů
Pro branding můžete také použít řetězcové literály, i když tento přístup je méně robustní než použití symbolů, protože řetězcové literály nejsou zaručeně jedinečné.
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Celkem USD:", totalUSD);
// Odkomentování následujícího řádku způsobí typovou chybu
// const invalidOperation = addUSD(usd1, eur1);
Tento příklad dosahuje stejného výsledku jako předchozí, ale používá řetězcové literály místo symbolů. I když je to jednodušší, je důležité zajistit, aby řetězcové literály použité pro branding byly v rámci vaší kódové báze jedinečné.
Praktické příklady a případy použití
Branded types lze aplikovat na různé scénáře, kde potřebujete vynutit typovou bezpečnost nad rámec strukturální kompatibility.
Identifikátory (ID)
Zvažte systém s různými typy ID, jako jsou UserID
, ProductID
a OrderID
. Všechny tyto identifikátory mohou být reprezentovány jako čísla nebo řetězce, ale chcete zabránit náhodnému smíchání různých typů ID.
const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };
const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };
function getUser(id: UserID): { name: string } {
// ... načtení dat uživatele
return { name: "Alice" };
}
function getProduct(id: ProductID): { name: string, price: number } {
// ... načtení dat produktu
return { name: "Example Product", price: 25 };
}
function createUserID(id: string): UserID {
return id as UserID;
}
function createProductID(id: string): ProductID {
return id as ProductID;
}
const userID = createUserID('user123');
const productID = createProductID('product456');
const user = getUser(userID);
const product = getProduct(productID);
console.log("Uživatel:", user);
console.log("Produkt:", product);
// Odkomentování následujícího řádku způsobí typovou chybu
// const invalidCall = getUser(productID);
Tento příklad ukazuje, jak mohou branded types zabránit předání ProductID
funkci, která očekává UserID
, čímž se zvyšuje typová bezpečnost.
Doménově specifické hodnoty
Branded types mohou být také užitečné pro reprezentaci doménově specifických hodnot s omezeními. Můžete mít například typ pro procenta, která by měla být vždy mezi 0 a 100.
const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };
function createPercentage(value: number): Percentage {
if (value < 0 || value > 100) {
throw new Error('Procento musí být mezi 0 a 100');
}
return value as Percentage;
}
function applyDiscount(price: number, discount: Percentage): number {
return price * (1 - discount / 100);
}
try {
const discount = createPercentage(20);
const discountedPrice = applyDiscount(100, discount);
console.log("Zlevněná cena:", discountedPrice);
// Odkomentování následujícího řádku způsobí chybu za běhu programu
// const invalidPercentage = createPercentage(120);
} catch (error) {
console.error(error);
}
Tento příklad ukazuje, jak vynutit omezení na hodnotu branded typu za běhu programu. Zatímco typový systém nemůže zaručit, že hodnota Percentage
je vždy mezi 0 a 100, funkce createPercentage
může toto omezení vynutit za běhu. Můžete také použít knihovny jako io-ts k vynucení validace branded typů za běhu.
Reprezentace data a času
Práce s daty a časy může být ošidná kvůli různým formátům a časovým pásmům. Branded types mohou pomoci rozlišit mezi různými reprezentacemi data a času.
const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };
const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };
function createUTCDate(dateString: string): UTCDate {
// Ověřit, že řetězec data je ve formátu UTC (např. ISO 8601 s Z)
if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
throw new Error('Neplatný formát data UTC');
}
return dateString as UTCDate;
}
function createLocalDate(dateString: string): LocalDate {
// Ověřit, že řetězec data je v lokálním formátu (např. RRRR-MM-DD)
if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
throw new Error('Neplatný formát lokálního data');
}
return dateString as LocalDate;
}
function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
// Provede konverzi časového pásma
const date = new Date(utcDate);
const localDateString = date.toLocaleDateString();
return createLocalDate(localDateString);
}
try {
const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
const localDate = convertUTCDateToLocalDate(utcDate);
console.log("Datum UTC:", utcDate);
console.log("Lokální datum:", localDate);
} catch (error) {
console.error(error);
}
Tento příklad rozlišuje mezi daty UTC a lokálními daty, čímž zajišťuje, že v různých částech vaší aplikace pracujete se správnou reprezentací data a času. Validace za běhu zajišťuje, že těmto typům mohou být přiřazeny pouze správně naformátované řetězce data.
Osvědčené postupy pro používání Branded Types
Pro efektivní používání branded types v TypeScriptu zvažte následující osvědčené postupy:
- Používejte symboly pro branding: Symboly poskytují nejvyšší záruku jedinečnosti a snižují riziko typových chyb.
- Vytvářejte pomocné funkce: Používejte pomocné funkce k vytváření hodnot branded typů. To poskytuje centrální bod pro validaci a zajišťuje konzistenci.
- Aplikujte validaci za běhu: Ačkoli branded types zvyšují typovou bezpečnost, nezabraňují přiřazení nesprávných hodnot za běhu. Používejte validaci za běhu k vynucení omezení.
- Dokumentujte branded types: Jasně dokumentujte účel a omezení každého branded typu pro zlepšení udržovatelnosti kódu.
- Zvažte dopady na výkon: Branded types přinášejí malou režii kvůli průnikovému typu a potřebě pomocných funkcí. Zvažte dopad na výkon v kritických částech vašeho kódu.
Výhody Branded Types
- Zvýšená typová bezpečnost: Zabraňuje náhodnému smíchání strukturálně podobných, ale logicky odlišných typů.
- Zlepšená srozumitelnost kódu: Činí kód čitelnějším a srozumitelnějším díky explicitnímu rozlišování mezi typy.
- Snížení počtu chyb: Zachycuje potenciální chyby již při kompilaci, což snižuje riziko chyb za běhu.
- Zvýšená udržovatelnost: Usnadňuje údržbu a refaktorování kódu díky jasnému oddělení zodpovědností.
Nevýhody Branded Types
- Zvýšená složitost: Přidává složitost do kódové báze, zejména při práci s mnoha branded typy.
- Režie za běhu: Přináší malou režii za běhu kvůli potřebě pomocných funkcí a validace.
- Potenciál pro boilerplate kód: Může vést k opakujícímu se kódu (boilerplate), zejména při vytváření a validaci branded typů.
Alternativy k Branded Types
Ačkoli jsou branded types mocnou technikou pro dosažení nominálního typování v TypeScriptu, existují alternativní přístupy, které můžete zvážit.
Neprůhledné typy (Opaque Types)
Neprůhledné typy jsou podobné branded typům, ale poskytují explicitnější způsob, jak skrýt podkladový typ. TypeScript nemá vestavěnou podporu pro neprůhledné typy, ale můžete je simulovat pomocí modulů a soukromých symbolů.
Třídy
Použití tříd může poskytnout objektově orientovanější přístup k definování odlišných typů. Ačkoli jsou třídy v TypeScriptu typovány strukturálně, nabízejí jasnější oddělení zodpovědností a lze je použít k vynucení omezení prostřednictvím metod.
Knihovny jako `io-ts` nebo `zod`
Tyto knihovny poskytují sofistikovanou validaci typů za běhu a mohou být kombinovány s branded typy k zajištění bezpečnosti jak při kompilaci, tak za běhu.
Závěr
TypeScript branded types jsou cenným nástrojem pro zvýšení typové bezpečnosti a srozumitelnosti kódu ve strukturálním typovém systému. Přidáním "značky" k typu můžete vynutit nominální typování a zabránit náhodnému smíchání strukturálně podobných, ale logicky odlišných typů. Ačkoli branded types přinášejí určitou složitost a režii, výhody zlepšené typové bezpečnosti a udržovatelnosti kódu často převažují nad nevýhodami. Zvažte použití branded typů ve scénářích, kde potřebujete zajistit, aby hodnota patřila ke konkrétnímu typu bez ohledu na její strukturu.
Porozuměním principům strukturálního a nominálního typování a uplatněním osvědčených postupů uvedených v tomto článku můžete efektivně využít branded types k psaní robustnějšího a udržovatelnějšího kódu v TypeScriptu. Od reprezentace měn a ID až po vynucování doménově specifických omezení poskytují branded types flexibilní a mocný mechanismus pro zvýšení typové bezpečnosti ve vašich projektech.
Při práci s TypeScriptem prozkoumejte různé techniky a knihovny dostupné pro validaci a vynucování typů. Zvažte použití branded typů ve spojení s knihovnami pro validaci za běhu, jako jsou io-ts
nebo zod
, abyste dosáhli komplexního přístupu k typové bezpečnosti.